package com.flow.framework.common.util.file;

import com.flow.framework.common.error.SystemErrorCode;
import com.flow.framework.common.util.verify.VerifyUtil;
import com.flow.framework.common.constant.FrameworkCommonConstant;
import com.flow.framework.common.exception.CheckedException;
import com.flow.framework.common.stream.BatchProcessInputStream;
import com.flow.framework.common.stream.handler.BatchReturnProcessHandler;
import com.flow.framework.common.util.io.IoUtil;
import lombok.extern.slf4j.Slf4j;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.model.enums.CompressionLevel;
import net.lingala.zip4j.model.enums.CompressionMethod;
import net.lingala.zip4j.model.enums.EncryptionMethod;

import javax.annotation.Nullable;
import java.io.*;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;

/**
 * 文件工具类
 *
 * @author luoguopiao
 * @version 0.0.1
 * @date 2022/1/3
 */
@Slf4j
public final class FileUtil {

    /**
     * The file copy buffer size (30 MB)
     */
    private static final long FILE_COPY_BUFFER_SIZE = 1024 * 1024 * 30;

    private static final List<Charset> ZIP_SUPPORT_CHARSETS = new ArrayList<Charset>() {{
        add(Charset.forName("GBK"));
        add(StandardCharsets.UTF_8);
    }};

    /**
     * 使用原生NIO复制文件
     *
     * @param srcFile       原始文件
     * @param destFile      复制后的文件
     * @param errorCallback 出错回调
     * @param <E>           异常
     */
    public static <E extends CheckedException> void copy(File srcFile, File destFile, Function<Exception, E> errorCallback) {
        FileChannel inputChannel = null;
        FileChannel outputChannel = null;
        try {
            inputChannel = new FileInputStream(srcFile).getChannel();
            outputChannel = new FileOutputStream(destFile).getChannel();
            long size = inputChannel.size();
            long pos = 0;
            long count;
            while (pos < size) {
                count = Math.min(size - pos, FILE_COPY_BUFFER_SIZE);
                pos += outputChannel.transferFrom(inputChannel, pos, count);
            }
        } catch (Exception e) {
            log.error("copy file error. file name : {}", srcFile.getName(), e);
            throw errorCallback.apply(e);
        } finally {
            IoUtil.close(inputChannel, outputChannel);
        }
    }

    /**
     * 将文件写入到输出流
     *
     * @param srcFile       原始文件
     * @param outputStream  输出流
     * @param errorCallback 出错回调
     * @param <E>           异常
     */
    public static <E extends CheckedException> void handleFileToOutputStream(File srcFile, OutputStream outputStream,
                                                                             Function<Exception, E> errorCallback) {
        InputStream inputStream = null;
        try {
            inputStream = new BufferedInputStream(new FileInputStream(srcFile));
            byte[] buffer = new byte[FrameworkCommonConstant.DEFAULT_IO_BUFFER_SIZE];
            doWrite(inputStream, outputStream, buffer);
            outputStream.flush();
        } catch (Exception e) {
            log.error("copy file to output stream error. file name : {}", srcFile.getName(), e);
            throw errorCallback.apply(e);
        } finally {
            IoUtil.close(inputStream);
        }
    }

    /**
     * 将文件处理并写入到输出流
     *
     * @param srcFile                   原始文件
     * @param outputStream              输出流
     * @param batchReturnProcessHandler 批量处理器
     * @param errorCallback             出错回调
     * @param <E>                       异常
     */
    public static <E extends CheckedException> void handleFileToOutputStream(File srcFile, OutputStream outputStream,
                                                                             BatchReturnProcessHandler batchReturnProcessHandler,
                                                                             Function<Exception, E> errorCallback) {
        InputStream inputStream = null;
        try {
            inputStream = new BatchProcessInputStream(new FileInputStream(srcFile), batchReturnProcessHandler);
            int batchSize = batchReturnProcessHandler.getBatchSize();
            byte[] buffer = new byte[batchSize];
            doWrite(inputStream, outputStream, buffer);
            outputStream.flush();
        } catch (Exception e) {
            log.error("copy file to output stream error. file name : {}", srcFile.getName(), e);
            throw errorCallback.apply(e);
        } finally {
            IoUtil.close(inputStream);
        }
    }

    private static void doWrite(InputStream inputStream, OutputStream outputStream, byte[] buffer) throws IOException {
        int len;
        while (FrameworkCommonConstant.EOF != (len = inputStream.read(buffer))) {
            outputStream.write(buffer, 0, len);
        }
    }

    /**
     * 删除文件或文件夹
     *
     * @param file          需要删除的文件或者文件夹
     * @param errorCallback 出错回调
     * @param <E>           异常
     */
    public static <E extends CheckedException> void deleteFileOrDirectory(File file, Function<Exception, E> errorCallback) {
        if (!file.exists()) {
            return;
        }

        if (file.isDirectory()) {
            File[] subFiles = file.listFiles();
            if (!VerifyUtil.isEmpty(subFiles)) {
                for (File subFile : subFiles) {
                    deleteFileOrDirectory(subFile, errorCallback);
                }
            }
        }
        if (!file.delete()) {
            log.error("delete file error. file name : {}", file.getName());
            throw errorCallback.apply(new IOException("delete file error."));
        }
    }

    /**
     * 删除文件或文件夹
     *
     * @param file 需要删除的文件或者文件夹
     * @return 是否删除成功
     */
    public static boolean deleteFileOrDirectory(File file) {
        if (!file.exists()) {
            return true;
        }

        if (file.isDirectory()) {
            File[] subFiles = file.listFiles();
            if (!VerifyUtil.isEmpty(subFiles)) {
                for (File subFile : subFiles) {
                    if (!deleteFileOrDirectory(subFile)) {
                        log.error("delete file error. file name : {}", subFile.getName());
                        return false;
                    }
                }
            }
        }
        if (!file.delete()) {
            log.error("delete file error. file name : {}", file.getName());
            return false;
        }
        return true;
    }

    /**
     * 加密zip格式压缩文件或者文件夹
     *
     * @param sourceFileOrDir 原始文件夹或者文件
     * @param targetZipFile   压缩后的压缩文件
     * @param password        密码
     * @param errorCallback   出错回调
     * @param <E>             异常
     */
    public static <E extends CheckedException> void zipWithPassword(File sourceFileOrDir, File targetZipFile, String password,
                                                                    Function<Exception, E> errorCallback) {
        if (null == sourceFileOrDir || !sourceFileOrDir.exists()) {
            log.error("source file or dir is null or source file path isn't exist.");
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }
        if (null == targetZipFile || !targetZipFile.getParentFile().exists()) {
            log.error("target zip file  is null or target zip file parent path isn't exist.");
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }
        if (VerifyUtil.isEmpty(password)) {
            log.error("password  is empty.");
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }
        zipCompress(sourceFileOrDir,
                targetZipFile,
                (file, zipParameters) -> {
                    //开启加密
                    zipParameters.setEncryptFiles(Boolean.TRUE);
                    zipParameters.setEncryptionMethod(EncryptionMethod.ZIP_STANDARD);
                    return new ZipFile(file, password.toCharArray());
                },
                errorCallback);
    }

    /**
     * zip格式压缩文件或者文件夹
     *
     * @param sourceFileOrDir 原始文件夹或者文件
     * @param targetZipFile   压缩后的压缩文件
     * @param errorCallback   出错回调
     * @param <E>             异常
     */
    public static <E extends CheckedException> void zip(File sourceFileOrDir, File targetZipFile,
                                                        Function<Exception, E> errorCallback) {
        if (null == sourceFileOrDir) {
            log.error("source file or dir is null.");
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }
        if (null == targetZipFile) {
            log.error("target zip file  is null.");
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }
        if (!sourceFileOrDir.exists()) {
            log.error("source file path isn't exist.");
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }
        zipCompress(sourceFileOrDir, targetZipFile, (file, zipParameters) -> new ZipFile(file), errorCallback);
    }

    private static <E extends CheckedException> void zipCompress(File sourceFileOrDir, File targetZipFile,
                                                                 BiFunction<File, ZipParameters, ZipFile> zipFileFunction,
                                                                 Function<Exception, E> errorCallback) {
        ZipParameters zipParameters = new ZipParameters();

        //压缩方式
        zipParameters.setCompressionMethod(CompressionMethod.DEFLATE);

        //压缩级别
        zipParameters.setCompressionLevel(CompressionLevel.MAXIMUM);

        ZipFile zipFile = null;
        try {
            zipFile = zipFileFunction.apply(targetZipFile, zipParameters);
            if (sourceFileOrDir.isDirectory()) {
                zipFile.addFolder(sourceFileOrDir, zipParameters);
            } else {
                zipFile.addFile(sourceFileOrDir, zipParameters);
            }
        } catch (Exception e) {
            log.error("zip with password error.", e);
            deleteFileOrDirectory(targetZipFile);
            throw errorCallback.apply(e);
        } finally {
            IoUtil.close(zipFile);
        }
    }

    /**
     * 解压zip
     *
     * @param srcZipFile    原始zip文件
     * @param destDirFile   需要解压到哪个目录
     * @param errorCallback 出错回调
     * @param <E>           异常
     */
    public static <E extends CheckedException> void unZip(File srcZipFile, File destDirFile, Function<Exception, E> errorCallback) {
        zipUnCompress(srcZipFile, destDirFile, null, errorCallback);
    }

    /**
     * 解压带密码的zip
     *
     * @param srcZipFile    原始zip文件
     * @param destDirFile   需要解压到哪个目录
     * @param password      解压密码
     * @param errorCallback 出错回调
     * @param <E>           异常
     */
    public static <E extends CheckedException> void unZipWithPassword(File srcZipFile, File destDirFile, String password, Function<Exception, E> errorCallback) {
        if (null == password) {
            log.error("password is null.");
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }
        zipUnCompress(srcZipFile, destDirFile, password, errorCallback);
    }

    private static <E extends CheckedException> void zipUnCompress(File srcZipFile, File destDirFile, @Nullable String password,
                                                                   Function<Exception, E> errorCallback) {
        if (null == srcZipFile) {
            log.error("src zip file is null.");
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }
        if (!srcZipFile.exists()) {
            log.error("src zip file isn't exist.");
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }
        if (null == destDirFile || !destDirFile.exists()) {
            log.error("dest dir file is null or dest dir isn't exist.");
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }
        ZipFile zipFile = null;
        try {
            if (null == password) {
                zipFile = new ZipFile(srcZipFile);
            } else {
                zipFile = new ZipFile(srcZipFile, password.toCharArray());
            }
            zipFile.setCharset(getZipCharset(srcZipFile, password));
            zipFile.extractAll(destDirFile.getPath());
        } catch (Exception e) {
            log.error("zip with password error.", e);
            throw errorCallback.apply(e);
        } finally {
            IoUtil.close(zipFile);
        }
    }


    private static Charset getZipCharset(File srcZipFile, @Nullable String password) throws Exception {
        for (Charset charset : ZIP_SUPPORT_CHARSETS) {
            ZipFile zipFile = null;
            try {
                if (null == password) {
                    zipFile = new ZipFile(srcZipFile);
                } else {
                    zipFile = new ZipFile(srcZipFile, password.toCharArray());
                }
                zipFile.setCharset(charset);
                List<FileHeader> fileHeaders = zipFile.getFileHeaders();
                boolean isTargetCharset = true;
                for (FileHeader fileHeader : fileHeaders) {
                    boolean canEncode = charset.newEncoder().canEncode(fileHeader.getFileName());
                    if (!canEncode) {
                        isTargetCharset = false;
                        break;
                    }
                }
                if (isTargetCharset) {
                    return charset;
                }
            } finally {
                IoUtil.close(zipFile);
            }
        }
        log.error("can't find target charset for zip. zip name : {}", srcZipFile.getName());
        throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
    }
}